home *** CD-ROM | disk | FTP | other *** search
/ NeXT Education Software Sampler 1992 Fall / NeXT Education Software Sampler 1992 Fall.iso / Programming / Source / PacMan / PacManView.m < prev    next >
Encoding:
Text File  |  1992-07-24  |  22.5 KB  |  696 lines

  1.  
  2. /* Generated by Interface Builder */
  3.  
  4. #import "PacManView.h"
  5. #import <libc.h>    // event stuff, misc.
  6. #import "FruitView.h"
  7. #import "PreferencesBrain.h"
  8. #import "Maze.h"
  9. #import "Monster.h"
  10. #import "Player.h"
  11. #import <appkit/graphics.h>    // for drawing
  12. #import <appkit/NXImage.h>    // for tiff rendering
  13. #import <appkit/Application.h>    // event stuff, misc.
  14.  
  15. // decides which screen (1-6) to load for a given level.  Allows us to
  16. // put them in an arbitrary order.
  17. static int screens[NUMSCREENS] = {    // which maze image (1-6) to use
  18.                       1, 1, 1, 1, 2, 2, 2, 2,
  19.                       3, 3, 3, 4, 4, 5, 5, 5,
  20.                       1, 2, 0, 0, 3, 4, 0, 5 };
  21.  
  22. // fruit values
  23. static long fruitval[NUMFRUITVALS + 1] = { 0, 100, 200, 300, 300, 500, 700,
  24.     700, 1000, 1000, 2000, 2000, 3000, 3000, 3000, 3000, 5000 };
  25.  
  26.  
  27. @implementation PacManView
  28.  
  29. - initFrame:(const NXRect *)frm    // designated initializer for a view
  30. {    
  31.     [super initFrame:frm];
  32.     begin = 128;
  33.  
  34. // initialize game variables
  35.     dotEat = NO;
  36.     
  37.     // where the maze is located within our view.
  38.     mazePos.x = 13; mazePos.y = 12;
  39.     myorigin.x = 0; myorigin.y = 0;
  40.     // a convenient rect; this way I don't have to keep creating a bunch of
  41.     // rects of dimensions GHOST_SIZE by GHOST_SIZE; I re-use this one...
  42.     NXSetRect(&eraseRect, 0, 0, GHOST_SIZE, GHOST_SIZE);
  43.     NXSetRect(&textEraseRect, 0, 0, 3 * GHOST_SIZE, 2 * GHOST_SIZE);
  44.     // make sure power dots get drawn
  45.     erasePwr = YES;
  46.     
  47.     return self;
  48. }
  49.  
  50. // Called by appDidInit to load up images, etc. that we need.
  51. - loadPix
  52. {
  53.     int i;
  54.     const int *gh;
  55.  
  56.     [super loadPix];
  57.     
  58.     [readyText setStringValue:""]; // clear out the TextField.
  59.     [fruitText setStringValue:""]; // clear out the TextField.
  60.     [self addSubview:readyText];
  61.     [self addSubview:fruitText];
  62.     fruitPointCount = 0;
  63.     
  64. // get textfields for displaying ghost point values
  65.     ghostText[0] = ghost1text;
  66.     ghostText[1] = ghost2text;
  67.     ghostText[2] = ghost3text;
  68.     ghostText[3] = ghost4text;
  69.     
  70. // build the ghost objects
  71.     gh = [maze ghosts]; 
  72.     for (i=0; i<=3; i++) {
  73.         ghost[i] = [[Monster alloc]
  74.                 initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]];
  75.         [ghostText[i] setStringValue:""]; // clear out the TextFields.
  76.         // It's impossible to get a color that makes this text stand out in
  77.         // all situations.  Turns out that whit is the best, since that's the
  78.         // maze color.  Trouble is, the "1" in "1600" disappears into the
  79.         // maze wall.  I suppose I could use tiffs that have white text
  80.         // ringed with black, but I prefer the TextFields myself.  Simpler.
  81.         //[ghostText[i] setTextColor:NX_COLORRED]; // make them all red.
  82.         [self addSubview:ghostText[i]];
  83.         ghostPointCount[i] = 0;
  84.     }
  85.     
  86. // get the images
  87.     fruit = [NXImage findImageNamed:"Fruit.tiff"];
  88.     gameOver = [NXImage findImageNamed:"GameOver.tiff"];
  89.     [gameOver getSize:&gameOverSize];
  90.  
  91.     return self;
  92. }
  93.  
  94. - ghost:(int)i                // return ghost #i
  95. {
  96.     return ghost[i];
  97. }
  98.  
  99.  
  100. // State machine...called by the timed entry.  This state machine basically
  101. // controls all the aspects of the game; depending upon it's state, different
  102. // things can/will happen.  The best way to figure this one out is to sit
  103. // down and draw a diagram show how the states transition from one to the
  104. // next.  Note that there are several counters, etc. that function as smaller
  105. // independent state machines that operate within the context of this larger
  106. // state machine.  (Dots blinking, monster states, etc. all run independently
  107. // from main state machine, although the main machine occasionally interrupts
  108. // things in the sub-machines.)  I've not yet had time to fully document the
  109. // logic involved in this state machine...if I ever have the time, I may do so,
  110. // but I don't know that anyone would care if I did anyway.  Minor changes in
  111. // here could render the entire game non-functional if you aren't careful!
  112. // UNLESS YOU KNOW WHAT YOU'RE DOING, DON'T MESS WITH THIS CODE!  Take time
  113. // to understand it fully before playing with it!
  114. // This is the most ridiculously long method you'll ever see, but there's
  115. // no really efficient way to break it up.  (Each section deals with what
  116. // happens in a specific state, and breaking into smaller methods isn't worth
  117. // while, since things are repeated...and it's silly to proliferate subroutines
  118. // that only get called once and from one place.)
  119. - autoUpdate:sender
  120. {            // ALL animation is controlled from here!!!
  121.     register int i, pts, lcnt;
  122.     int x, y;
  123.     BOOL flag = NO;
  124.     BOOL updateFlag = NO;
  125.     float gx, gy;
  126.  
  127.     // keep track of how many time we've been called; we can use it do
  128.     // decide when to do certain things...
  129.     if ([NXApp isHidden]) return self; // don't suck cycles if we're hidden
  130.     cycles++;
  131.     if (((![preferences speed]) || ([preferences speed] == 2))
  132.             && (!demoMode)) { // slow speed, so ignore 75% of entries
  133.         if (cycles & 0x03) return self;
  134.     }
  135.  
  136.     if (!(cycles & 0x0f)) { // power dots always blink no matter what.
  137.         [maze blinkPowerDot];    // happens every 16 cycles.
  138.         erasePwr = YES;
  139.     }
  140.     
  141.     if (fruitPointCount) {
  142.         if (!--fruitPointCount) {
  143.             [fruitText setStringValue:""]; // clear out the TextField.
  144.             fruitPointCount = WIPETEXT;
  145.     }    }
  146.  
  147.     for (i=0; i<4; i++) {
  148.         if (ghostPointCount[i]) {
  149.             if (!--ghostPointCount[i]) {
  150.                 [ghostText[i] setStringValue:""]; // clear out the TextField.
  151.                 ghostPointCount[i] = WIPETEXT;
  152.     }    }    }
  153.  
  154.     if (state == GAMEOVER) {
  155.     // when demowait counter hits WAITFORDEMO, we start up demo mode.
  156.     // it basically restarts the game--but with "demoMode" turned on.
  157.         if (demoWait++ == WAITFORDEMO) {
  158.             state = NORMALSTATE;
  159.             [self restartGame];
  160.             demoMode = YES;
  161.             [controller unpause];
  162.             [[self window] setTitle:"PacMan Demo"];
  163.             [controller zeroScore];
  164.     }    }
  165.     
  166.     // in this state, the "Get Ready!" sign has been put up; we stall for a]
  167.     // while and then we take it away and start up the next level.
  168.     if (state == READY) {
  169.         for (i=0; i<=3; i++) {
  170.             [ghost[i] move:self];
  171.         }
  172.         if (!(--begin)) {
  173.             state = NORMALSTATE;
  174.             [readyText setStringValue:""];
  175.             [controller nextLevel:self];
  176.             [player resetPlayer];
  177.             updateFlag = YES;
  178.     }    }
  179.  
  180.     // identical to the above, but we re-start on the same level without
  181.     // re-loading the maze...if we did, all the dots would be restored.
  182.     if (state == DIEREADY) {
  183.         for (i=0; i<=3; i++) {
  184.             [ghost[i] move:self];
  185.         }
  186.         if (!(--begin)) {
  187.             state = NORMALSTATE;
  188.             [readyText setStringValue:""];
  189.             [self startScreen];
  190.             [player resetPlayer];
  191.             updateFlag = YES;
  192.     }    }
  193.  
  194.     // make the maze blink on and off at the end of the level.
  195.     if (state == BLINK_LEVEL) {
  196.         if (begin--) {
  197.             if (begin < 32) {
  198.                 if (!(cycles & 0x03)) { // make the maze blink
  199.                     [maze visible:(![maze isVisible])];
  200.                     updateFlag = YES;
  201.             }    }
  202.         } else {
  203.             state = READY;
  204.             [maze playerPosition:&x :&y];
  205.             [readyText moveTo:(x - 2 * GHOST_SIZE + mazePos.x)
  206.                 :(y + mazePos.y)];
  207.             [readyText setStringValue:"Get Ready!"];
  208.             begin = 64;
  209.         }
  210.     }
  211.  
  212.     // stall.  when player dies, the player object needs time to get through
  213.     // it's whole sequence.
  214.     if (state == DYING_PAC) {
  215.         if (!(--begin)) {    // stall
  216.             if (![player newPlayer]) {    // no pacs left == game over.
  217.                 state = GAMEOVER;
  218.                 [controller gameOver];
  219.                 updateFlag = YES;
  220.             } else { // do ready, next pac...
  221.                 state = DIEREADY;
  222.                 [maze playerPosition:&x :&y];
  223.                 [readyText moveTo:(x - 2 * GHOST_SIZE + mazePos.x)
  224.                                  :(y + mazePos.y)];
  225.                 [readyText setStringValue:"Get Ready!"];
  226.                 begin = 64;
  227.     }    }    }
  228.  
  229.     // this state is the meat of the game.  move the player and monsters
  230.     //  and deal with player/ghost collisions and eating dots and fruit.
  231.     if (state == NORMALSTATE) {
  232.         // move all the ghosts and the pac
  233.         if (!paused) {
  234.             if (demoMode) lcnt = 2;
  235.             else {
  236.                 lcnt = [preferences speed] / 2 + 1;    // allow hyper speeds:
  237.             // since 0 <= speed <= 3, we'll move one or two frames per update
  238.             }
  239.             while (lcnt) {
  240.                 // figure out how to move player
  241.                 [player move:self];
  242.  
  243.                 // put up/take away the fruit
  244.                 if ((numFruits < 2)||demoMode) { // only two fruits per level
  245.                     if (fruitCount++ == timeToFruit) { // time for new fruit ?
  246.                         fruitOn = DRAW; numFruits++;
  247.                     }
  248.                     if (fruitCount == ERASEFRUIT) {
  249.                         // it's been there a while... so remove it
  250.                         timeToFruit = 200 + 16 * (random() & 0x0f);
  251.                         fruitOn = ERASE;
  252.                         fruitCount = 0;
  253.                     }
  254.                 }
  255.             
  256.                 // handle eating dots
  257.                 if ([maze eatDotAt:[player xpos] :[player ypos]]) {
  258.                     dotEat = YES;
  259.                 } else dotEat = NO;
  260.                 if ([maze powerDotAt:[player xpos] :[player ypos]]) {
  261.                     flag = YES;
  262.                     dotEat = YES;
  263.                 }
  264.                 if (![maze dots]) {
  265.                     state = BLINK_LEVEL;
  266.                     begin = 64;
  267.                 }
  268.  
  269.                 [maze playerPosition:&x :&y]; // get position of fruit
  270.                 // see if player ate the fruit
  271.                 if (fruitOn == YES) { // fruit's there...
  272.                     if ((abs([player ypos] - y) < GHOST_SIZE / 2) &&
  273.                         (abs([player xpos] - x) < GHOST_SIZE / 2)) { // ate it!
  274.                         fruitOn = ERASE;
  275.                         // add value of fruit to the score
  276.                         pts = (([controller level] < NUMFRUITVALS)
  277.                             ? fruitval[[controller level]]
  278.                             : fruitval[NUMFRUITVALS]);
  279.                         [controller addToScore:pts];
  280.                         // tell the player how many points he/she got
  281.                         ftx = x + TEXTOFFSET + mazePos.x;
  282.                         fty = y + mazePos.y;
  283.                         [fruitText moveTo:ftx :fty];
  284.                         if (pts) [fruitText setIntValue:pts];
  285.                         // now, align coords to the maze so erase
  286.                         // does it's job right (otherwise, we end up SOVERing
  287.                         // maze parts twice, and it looks _ugly_!
  288.                         ftx -= mazePos.x; fty -= mazePos.y;
  289.                         // chop off lower four bits (floor to mult. of 16)
  290.                         ftx /= 16; ftx = (ftx << 4) + mazePos.x;
  291.                         fty /= 16; fty = (fty << 4) + mazePos.y;
  292.                         fruitPointCount = 48;
  293.                 }    }
  294.  
  295.                 // check for player/ghost collision: (and deal with it)
  296.                 if ([maze dots]) {
  297.                     for (i=0; i<=3; i++) {
  298.                         // tell ghost of power dot
  299.                         if (flag) [ghost[i] powerDot:YES];
  300.                         [ghost[i] at:&gx :&gy];
  301.                         if ((abs(gx - [player xpos]) < GHOST_SIZE / 2) &&
  302.                             (abs(gy - [player ypos]) < GHOST_SIZE / 2)) {
  303.                         // collision! decide who dies...
  304.                             switch ([ghost[i] munch]) {
  305.                                 case YES : {    // player got ghost
  306.                                     // (-munch already added any bonus.)
  307.                                     gtx[i] = gx + TEXTOFFSET + mazePos.x;
  308.                                     gty[i] = gy + mazePos.y;
  309.                                     [ghostText[i] moveTo:gtx[i] :gty[i]];
  310.                                     [ghostText[i]
  311.                                         setIntValue:[controller ateGhost]];
  312.                                     // now, align to maze as above
  313.                                     gtx[i] -= mazePos.x; gtx[i] /= 16;
  314.                                     gty[i] -= mazePos.y; gty[i] /= 16;
  315.                                     gtx[i] = (gtx[i] << 4) + mazePos.x;
  316.                                     gty[i] = (gty[i] << 4) + mazePos.y;
  317.                                     ghostPointCount[i] = 48;
  318.                                     break;
  319.                                 }
  320.                                 case NO : {        // ghost got player, so die...
  321.                                     state = DYING_PAC;
  322.                                     begin = 48; // stall
  323.                                     [player pacDie];
  324.                                     fruitOn = NO;
  325.                                     updateFlag = YES;
  326.                                     break;
  327.                                 }
  328.                                 case HARMLESS : 
  329.                                 default : {        // eyes do nothing.
  330.                                     break;
  331.                 }    }    }    }    }
  332.  
  333.                 // figure out where ghosts will go next
  334.                 for (i=0; i<=3; i++) {
  335.                     [ghost[i] move:self];
  336.                 }
  337.                 if (lcnt > 1) { // make movement take effect w/o render
  338.                     for (i=0; i<=3; i++) {
  339.                         [ghost[i] moveOneFrame];
  340.                     }
  341.                     [player moveOneFrame];
  342.                 }
  343.                 lcnt--;
  344.     }    }    }
  345.     // draw all the changes, if applicable.
  346.     if (updateFlag) [self update];
  347.     if ((state != READY) && (state != DIEREADY)) [self updateSelf:&bounds :1];
  348.     return self;
  349. }
  350.  
  351. // This renders the whole screen, much like updateSelf:: below, but since
  352. // it always redraws the _entire_ screen, it is unnaceptably inefficient for
  353. // handling individual animation frames.
  354. - drawSelf:(NXRect *)rects :(int)rectCount    // redraws the screen.
  355. {        // right now, it's stupid and always redraws the whole view.
  356.     register int i, f;
  357.     int x, y;
  358.     NXPoint pos;
  359.     NXRect from, bezel, mazeRect;
  360.  
  361.     if ([self window] == nil)
  362.         return self;    // exit if no window to draw in
  363.     
  364.     [self lockFocus];
  365.         
  366.     // draw bezel
  367.     NXSetRect(&bezel, bounds.origin.x + 5, bounds.origin.y + 5,
  368.         bounds.size.width - 10, bounds.size.height - 10);
  369.     NXDrawGrayBezel(&bezel, &bounds);
  370.     NXFrameRectWithWidth(&bounds, 5);
  371.     bezel.size.width -= 6;
  372.     bezel.size.height -= 6;
  373.     bezel.origin.x += 3;
  374.     bezel.origin.y += 3;
  375.     [self drawBackground:&bezel]; // put background inside the bezel
  376.     NXSetRect(&mazeRect, 0, 0,
  377.         GHOST_SIZE * BLOCK_WIDTH, GHOST_SIZE * BLOCK_HEIGHT);
  378.     pos.x = 13; pos.y = 12;
  379.     [maze render:&mazeRect at:&mazePos]; // draw the maze
  380.     if ((fruitOn && (fruitOn != ERASE)) && (state != BLINK_LEVEL)) {
  381.         // render the fruit if necessary
  382.         // first, get coordinates of where to draw the fruit
  383.         [maze playerPosition:&x :&y];
  384.         pos.x = x + mazePos.x;
  385.         pos.y = y + mazePos.y;
  386.         // decide which fruit to draw
  387.         f = fruits[(([controller level] > FRUIT_LEVELS) ?
  388.             FRUIT_LEVELS : [controller level])];
  389.         // draw the fruit
  390.         NXSetRect(&from,
  391.             (f % FRUIT_PER_ROW) * FRUIT_SIZE,
  392.             (f / FRUIT_PER_ROW) * FRUIT_SIZE,
  393.             FRUIT_SIZE, FRUIT_SIZE);
  394.         [fruit composite:NX_SOVER fromRect:&from toPoint:&pos];
  395.         fruitOn = YES;
  396.     }
  397.     if (fruitOn == ERASE) fruitOn = NO;
  398.     if ((state != BLINK_LEVEL) || ((state == BLINK_LEVEL) && (begin > 31)))
  399.         [player renderAt:mazePos.x :mazePos.y move:NO];    // draw the pacman
  400.     if ((state != DYING_PAC) && (state != BLINK_LEVEL) &&
  401.             (state != GAMEOVER)) {    // draw the ghosts
  402.         for (i=0; i<=3; i++) {
  403.             [ghost[i] renderAt:mazePos.x :mazePos.y move:NO];
  404.     }    }
  405.  
  406.     [self unlockFocus];
  407.     if (fruitPointCount) [fruitText display];
  408.     for (i=0; i<4; i++) if (ghostPointCount[i]) [ghostText[i] display];
  409.     [self lockFocus];
  410.     if (state == GAMEOVER) {
  411.         pos.x = (NX_WIDTH(&bounds) - gameOverSize.width) / 2;
  412.         pos.y = (NX_HEIGHT(&bounds) - gameOverSize.height) / 2;
  413.         [gameOver composite:NX_SOVER toPoint:&pos];
  414.     }
  415.     [self unlockFocus];
  416.     NXPing();
  417.     return self;
  418. }
  419.  
  420. // This is the main drawing here.  It works like drawSelf, but only
  421. // _changes_ stuff; it doesn't re-draw the whole view each time.  This
  422. // has been done to speed things up.  We erase the old, and then redraw
  423. // all the spots we erased after calculating where things have moved to.
  424. - updateSelf:(NXRect *)rects :(int)rectCount    // redraws the screen.
  425. {        // it redraws only what has changed since last redraw.
  426.     register int f, i;
  427.     int x, y;
  428.     const int *pd;    // pointer to array of power dot coords
  429.     NXRect from;
  430.     NXPoint pos;
  431.  
  432.     if ([self window] == nil) return self;    // exit if no window to draw in
  433.         
  434.     [self lockFocus];
  435.     pd = [maze powerDot];    // get power dot coords
  436.     // erase ghosts and power dots by drawing background image on top.
  437.     for (i=0; i<=3; i++) {
  438.         if ((ghostPointCount[i] == WIPETEXT) && (state != GAME_OVER)) {
  439.             ghostPointCount[i] = 0;
  440.             ZAPRECT(textEraseRect, gtx[i], gty[i]);
  441.             NX_X(&textEraseRect) -= mazePos.x;
  442.             NX_Y(&textEraseRect) -= mazePos.y;
  443.             [maze render:&textEraseRect at:&mazePos];    // render maze there
  444.         }
  445.         if (state != GAME_OVER) {
  446.             [ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
  447.             NX_X(&eraseRect) += mazePos.x;
  448.             NX_Y(&eraseRect) += mazePos.y;
  449.             [self drawBackground:&eraseRect];
  450.         } if (erasePwr) {    // erase power dot every time it blinks.
  451.             NX_X(&eraseRect) = pd[i * 2 ]    * GHOST_SIZE + mazePos.x;
  452.             NX_Y(&eraseRect) = pd[i * 2 + 1] * GHOST_SIZE + mazePos.y;
  453.             [self drawBackground:&eraseRect];
  454.     }    }
  455.     
  456.     // erase fruit text
  457.     if ((fruitPointCount == WIPETEXT) && (state != GAME_OVER)) {
  458.         fruitPointCount = 0;
  459.         ZAPRECT(textEraseRect, ftx, fty);
  460.         NX_X(&textEraseRect) -= mazePos.x;
  461.         NX_Y(&textEraseRect) -= mazePos.y;
  462.         [maze render:&textEraseRect at:&mazePos];    // render maze there
  463.     }
  464.     
  465.     // erase fruit if needed
  466.     if ((fruitOn) && (state != GAME_OVER)) {
  467.         // inefficient -- we always re draw it when it's on...
  468.         [maze playerPosition:&x :&y];
  469.         NX_X(&eraseRect) = x + mazePos.x;
  470.         NX_Y(&eraseRect) = y + mazePos.y; 
  471.         [self drawBackground:&eraseRect];
  472.     }
  473.  
  474.     // erase the player's PacMan
  475.     if (state != GAME_OVER) {
  476.         [player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
  477.         NX_X(&eraseRect) += mazePos.x;
  478.         NX_Y(&eraseRect) += mazePos.y;
  479.         [self drawBackground:&eraseRect];
  480.         [player lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
  481.         [maze render:&eraseRect at:&mazePos];    // render maze
  482.     } if (dotEat) {    // makes sure that the eaten dot was erased.
  483.         // if I don't do this, when a dot is eaten, if the player quickly
  484.         // reverses direction, the dot will remain--looking half eaten, even
  485.         // though we internally register it as eaten.
  486.         [maze lastDot:&x :&y];
  487.         NX_X(&eraseRect) = x + mazePos.x;
  488.         NX_Y(&eraseRect) = y + mazePos.y; 
  489.         [self drawBackground:&eraseRect];    // draw background over it
  490.         NX_X(&eraseRect) = x;
  491.         NX_Y(&eraseRect) = y; 
  492.         [maze render:&eraseRect at:&mazePos];    // render maze there
  493.         dotEat = NO;
  494.     }
  495.     
  496.     // render maze underneath the fruit if necessary
  497.     if ((fruitOn) && (state != GAME_OVER)) {
  498.         // inefficient -- we always re draw it when it's on...
  499.         [maze playerPosition:&x :&y];
  500.         NX_X(&eraseRect) = x;
  501.         NX_Y(&eraseRect) = y; 
  502.         [maze render:&eraseRect at:&mazePos];    // render maze there
  503.     }
  504.     
  505.     // draw the maze over where the ghosts were.
  506.     for (i=0; i<=3; i++) {
  507.         [ghost[i] lastAt:&NX_X(&eraseRect) :&NX_Y(&eraseRect)];
  508.         [maze render:&eraseRect at:&mazePos];
  509.         if (erasePwr) { // will put power dot on with blinks.
  510.             NX_X(&eraseRect) = pd[i * 2 ]    * GHOST_SIZE;
  511.             NX_Y(&eraseRect) = pd[i * 2 + 1] * GHOST_SIZE;
  512.             [maze render:&eraseRect at:&mazePos];
  513.     }    }
  514.  
  515.     // render the fruit if necessary
  516.     if (fruitOn) {
  517.         if ((fruitOn != ERASE) && (state != BLINK_LEVEL)) {
  518.             // inefficient -- we always re draw it when it's on...
  519.             // first, get coordinates of where to draw the fruit
  520.             [maze playerPosition:&x :&y];
  521.             pos.x = x + mazePos.x;
  522.             pos.y = y + mazePos.y;
  523.             // decide which fruit to draw
  524.             f = fruits[(([controller level] > FRUIT_LEVELS) ?
  525.                 FRUIT_LEVELS : [controller level])];
  526.             // draw the fruit
  527.             NXSetRect(&from,
  528.                 (f % FRUIT_PER_ROW) * FRUIT_SIZE,
  529.                 (f / FRUIT_PER_ROW) * FRUIT_SIZE,
  530.                 FRUIT_SIZE, FRUIT_SIZE);
  531.             [fruit composite:NX_SOVER fromRect:&from toPoint:&pos];
  532.             fruitOn = YES;
  533.         } else { fruitOn = NO; }
  534.     }
  535.     
  536.     // put the player's Pac on the screen if in a state where it's visible.
  537.     if ((state != BLINK_LEVEL) || ((state == BLINK_LEVEL) && (begin > 31)))
  538.         [player renderAt:mazePos.x :mazePos.y
  539.             move:((!paused) && (state == NORMALSTATE))];
  540.         
  541.     // put ghosts on screen if we're in a state where they are visible.
  542.     if ((state != DYING_PAC) && (state != BLINK_LEVEL) &&
  543.             (state != GAMEOVER)) {
  544.         for (i=0; i<=3; i++) {
  545.             [ghost[i] renderAt:mazePos.x :mazePos.y move:
  546.                 ((!paused) && ((state == READY) || (state == NORMALSTATE)))];
  547.     }    }
  548.     
  549.     /*if (state == GAMEOVER) {
  550.         pos.x = (NX_WIDTH(&bounds) - gameOverSize.width) / 2;
  551.         pos.y = (NX_HEIGHT(&bounds) - gameOverSize.height) / 2;
  552.         [gameOver composite:NX_SOVER toPoint:&pos];
  553.     }*/
  554.     // housekeeping for the graphics:
  555.     [self unlockFocus];
  556.     if (fruitOn == ERASE) fruitOn = NO;
  557.     if (fruitPointCount) [fruitText display];
  558.     for (i=0; i<4; i++) if (ghostPointCount[i]) [ghostText[i] display];
  559.     [[self window] flushWindow];
  560.     NXPing();
  561.     erasePwr = NO;    // we've listened to this flag, so turn it off now.
  562.     return self;
  563. }
  564.  
  565. // Handle player movement.  We respond to the arrow keys.  If we don't
  566. // recognize the key, we pass it on.  This method just shunts the key
  567. // off to the player object, which is what really deals with it. 
  568. - keyDown:(NXEvent *)myevent
  569. {
  570.     if (!myevent) return self; // if no event when coalescing, go away
  571.     if (!(myevent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|NX_COMMANDMASK)) ) {
  572.         if          (myevent->data.key.keyCode == 0x16) { // Up Arrow
  573.             [player newDirection:PAC_UP];
  574.             [controller unpause];
  575.             return self;
  576.         } else if (myevent->data.key.keyCode == 0x0f) { // Down Arrow
  577.             [player newDirection:PAC_DOWN];
  578.             [controller unpause];
  579.             return self;
  580.         } else if (myevent->data.key.keyCode == 0x09) { // Left Arrow
  581.             [player newDirection:PAC_LEFT];
  582.             [controller unpause];
  583.             return self;
  584.         } else if (myevent->data.key.keyCode == 0x10) { // Right Arrow
  585.             [player newDirection:PAC_RIGHT];
  586.             [controller unpause];
  587.             return self;
  588.         } else [super keyDown:myevent];
  589.     } else [super keyDown:myevent];
  590.     [self keyDown:[NXApp peekAndGetNextEvent:NX_KEYDOWN]]; // coalesce keydowns
  591.     // this is done recursively...primitive, but easy to implement :-)
  592.     return self;
  593. }
  594.  
  595. // set up the screen at the start of a new level.  Loads in the maze.
  596. - setUpScreen
  597. {        
  598.     [super setUpScreen];
  599.     [maze makeMaze:screens[(([controller level] >= NUMSCREENS) ?
  600.         (NUMSCREENS - 1) : [controller level]) - 1]];
  601.     [self startScreen];
  602.     timeToFruit = 200 + 16 * (random() & 0x0f);
  603.     fruitOn = NO;
  604.     fruitCount = 0;
  605.     numFruits = 0;
  606.     
  607.     return self;
  608. }
  609.  
  610. // This let's us put the ghosts back where they are at the start of the level.
  611. // This is separate from above because the above also resets the dots, and if
  612. // the player dies, we only want to reset the ghosts, not the dots, too.
  613. - startScreen
  614. {
  615.     const int *gh; int i;
  616.         
  617.     gh = [maze ghosts]; // pointer to array of ghost coordinates
  618.     for (i=0; i<=3; i++) {    // re-initialize each ghost
  619.         [ghost[i] initGhost:i player:player maze:maze at:gh[i*2] :gh[i*2+1]];
  620.     }
  621.     // don't let fruit come out too quickly
  622.     if (timeToFruit - fruitCount < 60)
  623.         timeToFruit = 200 + 16 * (random() & 0x0f);
  624.     
  625.     return self;
  626. }
  627.  
  628. - restartGame
  629. {
  630.     int x, y;
  631.     
  632.     // go to READY state to start game; but want DIEREADY so we don't advance
  633.     // the level; the controller, by virtue of calling this method, has already
  634.     // advance the level.
  635.     state = DIEREADY;
  636.     begin = 96;
  637.     
  638.     // put up the "Get Ready!" sign.
  639.     [maze playerPosition:&x :&y];
  640.     [readyText moveTo:(x - 2 * GHOST_SIZE + mazePos.x) :(y + mazePos.y)];
  641.     [readyText setStringValue:"Get Ready!"];
  642.     
  643.     // make sure that all artifacts of demo mode are gone
  644.     demoWait = 0;
  645.     if (demoMode) {
  646.         demoMode = NO;
  647.         [[self window] setTitle:"PacMan"];
  648.     }
  649.     
  650.     [self setUpScreen];        // load maze, etc.
  651.     [controller zeroScore];    // clear the score
  652.     [self getPreferences];    // make sure we're up to date
  653.     [player newPlayer];        // get a new pac to play with
  654.     // (above always sets up the pac, but in demoMode, the gameBrain hasn't
  655.     // given up 3 pacs, so we end up taking a negative # of pacs, which means
  656.     // demo mode will end as soon as the pac dies.)
  657.     [self update];            // redraw screen
  658.     return self;
  659. }
  660.  
  661.  
  662. // These three methods load the three hardcoded background images.
  663. // To work, the images must be in the same directory as the executable.
  664. - back1:sender
  665. {
  666.     char *tempStr;
  667.     
  668.     tempStr = malloc(256); sprintf(tempStr, "%sGradation.eps", appPath);
  669.     [self setBackgroundFile:tempStr andRemember:YES];
  670.     free(tempStr); [self display];
  671.     return self;
  672. }
  673.  
  674. - back2:sender
  675. {
  676.     char *tempStr;
  677.     
  678.     tempStr = malloc(256); sprintf(tempStr, "%slush.tiff", appPath);
  679.     [self setBackgroundFile:tempStr andRemember:YES];
  680.     free(tempStr); [self display];
  681.     return self;
  682. }
  683.  
  684. - back3:sender
  685. {
  686.     char *tempStr;
  687.     
  688.     tempStr = malloc(256); sprintf(tempStr, "%sSunset.tiff", appPath);
  689.     [self setBackgroundFile:tempStr andRemember:YES];
  690.     free(tempStr); [self display];
  691.     return self;
  692. }
  693.  
  694.  
  695. @end
  696.